﻿// <copyright file="MapInfoFile.cs" company="QUT">
//     Copyright (c) Ninety Nine Percent. All rights reserved.
// </copyright>

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;

namespace Qut.GenerateKml.Data
{
    /// <summary>
    /// MapInfoFile represents the file structure of MapInfo .mif and .mid files
    /// </summary>
    public class MapInfoFile
    {
        /// <summary>
        /// MIF header clause name: version
        /// </summary>
        private const string HCLAUSE_VERSION = "version";

        /// <summary>
        /// MIF header clause name: charset
        /// </summary>
        private const string HCLAUSE_CHARSET = "charset";

        /// <summary>
        /// MIF header clause name: delimiter
        /// </summary>
        private const string HCLAUSE_DELIMITER = "delimiter";

        /// <summary>
        /// MIF header clause name: unique
        /// </summary>
        private const string HCLAUSE_UNIQUE = "unique";

        /// <summary>
        /// MIF header clause name: index
        /// </summary>
        private const string HCLAUSE_INDEX = "index";

        /// <summary>
        /// MIF header clause name: coordsys
        /// </summary>
        private const string HCLAUSE_COORDSYS = "coordsys";

        /// <summary>
        /// MIF header clause name: transform
        /// </summary>
        private const string HCLAUSE_TRANSFORM = "transform";

        /// <summary>
        /// MIF header clause name: columns
        /// </summary>
        private const string CLAUSE_COLUMNS = "columns";

        /// <summary>
        /// Geometry type identifier: none
        /// </summary>
        private const string TYPE_NONE = "none";

        /// <summary>
        /// Geometry type identifier: point
        /// </summary>
        private const string TYPE_POINT = "point";

        /// <summary>
        /// Geometry type identifier: line
        /// </summary>
        private const string TYPE_LINE = "line";

        /// <summary>
        /// Geometry type identifier: pline
        /// </summary>
        private const string TYPE_PLINE = "pline";

        /// <summary>
        /// Geometry type identifier: region
        /// </summary>
        private const string TYPE_REGION = "region";

        /// <summary>
        /// Geometry type identifier: text
        /// </summary>
        private const string TYPE_TEXT = "text";

        /// <summary>
        /// Draw Style: pen
        /// </summary>
        private const string CLAUSE_PEN = "pen";

        /// <summary>
        /// Draw Style: brush
        /// </summary>
        private const string CLAUSE_BRUSH = "brush";

        /// <summary>
        /// Draw Style: center
        /// </summary>
        private const string CLAUSE_CENTER = "center";

        /// <summary>
        /// This is true if the transform clause is used in the MIF header
        /// </summary>
        private bool useTransform = false;

        /// <summary>
        /// X multiplier for the transform clause
        /// </summary>
        private float multX = 1;

        /// <summary>
        /// Y multiplier for the transform clause
        /// </summary>
        private float multY = 1;

        /// <summary>
        /// X displacement for the transform clause
        /// </summary>
        private float sumX = 0;

        /// <summary>
        /// Y displacement for the transform clause
        /// </summary>
        private float sumY = 0;

        /// <summary>
        /// This private field represents the MIF header information
        /// </summary>
        private Hashtable mifHeader = new Hashtable();

        /// <summary>
        /// The MIF data table.
        /// </summary>
        private List<MapInfoRegion> mifData = new List<MapInfoRegion>();

        /// <summary>
        /// The MID data table.
        /// </summary>
        private DataTable midData = new DataTable("midData");

        /// <summary>
        /// The parameter.
        /// </summary>
        private Hashtable param = new Hashtable();

        /// <summary>
        /// The default char delimiter (tab) for the  MID.
        /// </summary>
        private char charDelimiter = '\t';

        /// <summary>
        /// MapInfo MIF tokeniser.
        /// </summary>
        private MapInfoFileTokeniser mif;

        /// <summary>
        /// MapInfo MID tokeniser.
        /// </summary>
        private MapInfoFileTokeniser mid;

        /// <summary>
        /// Initializes a new instance of the <see cref="MapInfoFile"/> class.
        /// </summary>
        /// <param name="mifFilePath">The mif file path.</param>
        /// <param name="midFilePath">The mid file path.</param>
        public MapInfoFile(string mifFilePath, string midFilePath)
        {
            try
            {
                this.mif = new MapInfoFileTokeniser(File.OpenText(mifFilePath));
                this.ReadMifHeader();
                this.ReadMifData();
                this.mid = new MapInfoFileTokeniser(File.OpenText(midFilePath));
                this.ReadMidData();
            }
            catch (FileNotFoundException e)
            {
                throw new FileNotFoundException("MIF file not found: " + e.Message);
            }
            catch (IOException e)
            {
                throw new IOException("Exception opening MIF file: " + e.Message);
            }
        }

        /// <summary>
        /// Gets the MapInfo mif header.
        /// </summary>
        /// <returns>Returns the MIF header.</returns>
        public Hashtable GetMifHeader()
        {
            return this.mifHeader;
        }

        /// <summary>
        /// Gets the MapInfo mif data.
        /// </summary>
        /// <returns>Returns the MIF data.</returns>
        public List<MapInfoRegion> GetMifData()
        {
            return this.mifData;
        }

        /// <summary>
        /// Gets the MapInfo mid data.
        /// </summary>
        /// <returns>Returns the MID data.</returns>
        public DataTable GetMidData()
        {
            return this.midData;
        }

        /// <summary>
        /// Gets the header clause.
        /// </summary>
        /// <param name="clause">The clause.</param>
        /// <returns>The header clause.</returns>
        public string GetHeaderClause(string clause)
        {
            return (string)this.mifHeader[clause];
        }

        /// <summary>
        /// Reads the mif header.
        /// </summary>
        private void ReadMifHeader()
        {
            while (this.mif.ReadLine())
            {
                string token = this.mif.GetToken().ToLower();
                if (token.Equals("data") && this.mif.GetLine().Equals(string.Empty))
                {
                    break;
                }

                if (token.Equals(HCLAUSE_VERSION))
                {
                    this.SetHeaderClause(HCLAUSE_VERSION, this.mif.GetLine());
                }

                if (token.Equals(HCLAUSE_CHARSET))
                {
                    this.SetHeaderClause(HCLAUSE_CHARSET, this.mif.GetToken(' ', false, true));
                }

                if (token.Equals(HCLAUSE_DELIMITER))
                {
                    this.SetHeaderClause(HCLAUSE_DELIMITER, this.mif.GetToken(' ', false, true));
                    string tmp = this.GetHeaderClause(HCLAUSE_DELIMITER);
                    this.charDelimiter = tmp[0];
                }

                if (token.Equals(HCLAUSE_UNIQUE))
                {
                    this.SetHeaderClause(HCLAUSE_UNIQUE, this.mif.GetLine());
                }

                if (token.Equals(HCLAUSE_COORDSYS))
                {
                    this.SetHeaderClause(HCLAUSE_COORDSYS, this.mif.GetLine());
                }

                if (token.Equals(HCLAUSE_INDEX))
                {
                    this.SetHeaderClause(HCLAUSE_INDEX, this.mif.GetLine());
                }

                if (token.Equals(HCLAUSE_TRANSFORM))
                {
                    this.useTransform = true;
                    this.multX = float.Parse("0" + this.mif.GetToken(','));
                    this.multY = float.Parse("0" + this.mif.GetToken(','));
                    this.sumX = float.Parse("0" + this.mif.GetToken(','));
                    this.sumY = float.Parse("0" + this.mif.GetToken(','));

                    if (this.multX == 0)
                    {
                        this.multX = 1;
                    }

                    if (this.multY == 0)
                    {
                        this.multY = 1;
                    }
                }

                if (token.Equals(CLAUSE_COLUMNS))
                {
                    int cols;

                    try
                    {
                        cols = int.Parse(this.mif.GetLine());
                    }
                    catch (FormatException)
                    {
                        throw new IOException("bad number of colums: " + this.mif.GetLine());
                    }

                    // Columns <n> does not take into account the geometry column, so I increment cols by 1
                    cols++;

                    string name;
                    string type;

                    for (int i = 1; i < cols; i++)
                    {
                        if (!this.mif.ReadLine())
                        {
                            throw new IOException("Expected column definition");
                        }

                        DataColumn column = new DataColumn();
                        name = this.mif.GetToken();
                        type = this.mif.GetToken('(').ToLower();

                        if (type.Equals("float") || type.Equals("decimal"))
                        {
                            column.DataType = typeof(double);
                            column.AllowDBNull = false;
                            column.Caption = name;
                            column.ColumnName = name;
                            column.DefaultValue = "0.0";
                            column.DataType = Type.GetType("System.Double");
                            column.AllowDBNull = false;
                            column.Caption = name;
                            column.ColumnName = name;
                            column.DefaultValue = "0.0";
                        }
                        else if (type.StartsWith("char"))
                        {
                            column.DataType = typeof(string);
                            column.AllowDBNull = false;
                            column.Caption = name;
                            column.ColumnName = name;
                            column.DefaultValue = string.Empty;
                        }
                        else if (type.Equals("integer") || type.Equals("smallint"))
                        {
                            column.DataType = typeof(int);
                            column.AllowDBNull = false;
                            column.Caption = name;
                            column.ColumnName = name;
                            column.DefaultValue = 0;
                        }
                        else if (type.Equals("logical"))
                        {
                            column.DataType = typeof(bool);
                            column.AllowDBNull = false;
                            column.Caption = name;
                            column.ColumnName = name;
                            column.DefaultValue = false;
                        }
                        else if (type.Equals("date"))
                        {
                            column.DataType = typeof(DateTime);
                            column.AllowDBNull = false;
                            column.Caption = name;
                            column.ColumnName = name;
                            column.DefaultValue = null;
                        }
                        else
                        {
                            column.DataType = typeof(string);
                            column.AllowDBNull = false;
                            column.Caption = name;
                            column.ColumnName = name;
                            column.DefaultValue = string.Empty;
                        }

                        this.midData.Columns.Add(column);
                    }
                }
            }
        }

        /// <summary>
        /// Reads the mif data.
        /// </summary>
        private void ReadMifData()
        {
            while (this.mif.ReadLine())
            {
                string objType = this.mif.GetToken().ToLower();
                if (objType.Equals(TYPE_REGION))
                {
                    this.ReadRegionObject();
                }
            }
        }

        /// <summary>
        /// Reads the MIF coordinate.
        /// </summary>
        /// <returns>Returns the MIF coordinate.</returns>
        private PointF ReadMIFCoordinate()
        {
            string x;
            string y;

            try
            {
                x = this.mif.GetToken(' ', true);
                y = this.mif.GetToken();

                if (x.Equals(string.Empty) || y.Equals(string.Empty))
                {
                    throw new IOException("End of file.");
                }

                PointF result = new PointF(float.Parse(x), float.Parse(y));

                if (this.useTransform)
                {
                    result.X = (result.X * this.multX) + this.sumX;
                    result.Y = (result.Y * this.multY) + this.sumY;
                }

                return result;
            }
            catch (Exception e)
            {
                throw new IOException("Error getting coordinates: " + e.ToString());
            }
        }

        /// <summary>
        /// Reads the region object.
        /// </summary>
        private void ReadRegionObject()
        {
            try
            {
                MapInfoRegion region = new MapInfoRegion();
                int numpolygons = Int32.Parse(this.mif.GetToken(' ', true));

                for (int i = 0; i < numpolygons; i++)
                {
                    int numpoints = Int32.Parse(this.mif.GetToken(' ', true));
                    MapInfoPolygon polygon = new MapInfoPolygon();
                    for (int p = 0; p < numpoints; p++)
                    {
                        PointF point = this.ReadMIFCoordinate();
                        polygon.AddPoint(point);
                    }

                    region.AddPolygon(polygon);
                }

                for (int i = 0; i < 3; i++)
                {
                    this.mif.ReadLine();
                    string drawStyle = this.mif.GetToken(' ', true).ToLower();
                    if (drawStyle == CLAUSE_PEN)
                    {
                        region.Pen = this.mif.GetToken(' ', true);
                    }

                    if (drawStyle == CLAUSE_BRUSH)
                    {
                        region.Brush = this.mif.GetToken(' ', true);
                    }

                    if (drawStyle == CLAUSE_CENTER)
                    {
                        region.Center = this.ReadMIFCoordinate();
                    }
                }

                this.mifData.Add(region);
            }
            catch (Exception e)
            {
                throw new IOException("Exception reading Region data from MIF file : " + e.ToString());
            }
        }

        /// <summary>
        /// Reads the mid data.
        /// </summary>
        private void ReadMidData()
        {
            while (this.mid.ReadLine())
            {
                string token;
                int cols = this.midData.Columns.Count;
                DataRow row = this.midData.NewRow();

                for (int i = 0; i < cols; i++)
                {
                    token = this.mid.GetToken(this.charDelimiter);
                    if (token.StartsWith("\"") && token.EndsWith("\"") && (token.Length > 1))
                    {
                        token = token.Substring(1, token.Length - 2).Replace("\"\"", "\"");
                    }

                    Type t = this.midData.Columns[i].DataType;
                    object tmp;
                    tmp = Convert.ChangeType(token, t);
                    row[this.midData.Columns[i].ColumnName] = tmp;
                }

                this.midData.Rows.Add(row);
            }
        }

        /// <summary>
        /// Sets the header clause.
        /// </summary>
        /// <param name="clause">The clause.</param>
        /// <param name="value">The value.</param>
        private void SetHeaderClause(string clause, string value)
        {
            if (value == null)
            {
                value = string.Empty;
            }

            if (clause.Equals(HCLAUSE_DELIMITER)
                    && (value.Equals(string.Empty) || value.Equals("\"")))
            {
                throw new Exception("Bad delimiter specified");
            }

            this.mifHeader.Add(clause, value);
        }

        /// <summary>
        /// Gets the param.
        /// </summary>
        /// <param name="name">The param name.</param>
        /// <param name="defa">The param defa.</param>
        /// <param name="required">if set to <c>true</c> [required].</param>
        /// <param name="param">The required parameter.</param>
        /// <returns>Returns the parameter.</returns>
        private object GetParam(string name, object defa, bool required, Hashtable param)
        {
            object result = null;
            try
            {
                result = param[name];
            }
            catch (Exception)
            {
                result = null;
            }

            if (result == null)
            {
                if (required)
                {
                    throw new Exception("MIFFile: parameter " + name
                        + " is required");
                }

                result = defa;
            }

            return result;
        }
    }
}
